這次好想工作室大概有二十多位夥伴參加鐵人賽,為此我們也拉了一個 slack channel 來討論和分享彼此的心得,除了互相取暖外,我們也互相激勵。比賽也進入了第六天,每天看看其他人發的文章狀況也就成了每個人的 daily mission,但每次都要打開十多個人的主題也是挺煩人的,既然有麻煩事,當個網頁工程師自幹個爬蟲也是挺理所當然的,那我們今天的任務就是寫個爬蟲來抓取所有工作是參賽者的狀況吧!
要看一位參賽主題,肯定要先有 url,而這個 url 會長這樣 https://ithelp.ithome.com.tw/users/20107159/ironman/1325 ,接著我們進去參賽主題的網頁就能看到一系列相關數字,這個任務看起來挺簡單的,在一個頁面就能得到所有資訊。
在這個頁面裡面,會有興趣的大概是以下幾項:
只需要一個 get request,那就不需要打開 postman 了,看來這個難度在於 dom 的選擇器上,我們必須在這個頁面裡面把我們需要的資選出來。用 dev tool 檢測一下參賽主題,發現他的 class 是 qa-list__title qa-list__title--ironman
,看起來用的 class 都是具有辨識性的。
接下來再觀察一下其他的相關項目,每個都具有辨識性的 class,這樣很便利於我們抓取資料,看起來稍微複雜一點的大概會是在文章列表。那我們就直接使用 dev tool 的 console 來測試選擇看看這些物件吧。
// 選手名稱 (name)
$('.profile-header__name').text().trim()
// 參賽主題 (title)
$('.qa-list__title--ironman').text().trim().replace(' 系列', '')
// 參賽天數 (joinDays)
$('.qa-list__info--ironman span').eq(0).text().replace(/[^0-9]/g,'')
// 參賽文章數 (posts)
$('.qa-list__info--ironman span').eq(1).text().replace(/[^0-9]/g,'')
// 追蹤人數 (subscriber)
$('.qa-list__info--ironman span').eq(2).text().replace(/[^0-9]/g,'')
文章列表的部分看起來會需要一個 loop 來抓取 class 為 qa-list profile-list ir-profile-list
的物件們,然後每個再去抓他們各自的文章相關資訊,來試試看能不能順利選擇文章列表。
$('.qa-list')
接下來看文章的資訊,這邊比較特別一點,文章的 like、留言、瀏覽並不是有個別的 class name,而是一起使用 qa-condition__count
class 來放這些數字,但我們應該可以用第幾個來辨識他們。
$('.qa-list').map((index, obj) => {
return {
title: $(obj).find('.qa-list__title').text().trim(),
like: $(obj).find('.qa-condition__count').eq(0).text().trim(),
comment: $(obj).find('.qa-condition__count').eq(1).text().trim(),
view: $(obj).find('.qa-condition__count').eq(2).text().trim(),
date: $(obj).find('.qa-list__info-time').text().trim(),
url: $(obj).find('.qa-list__title a').attr('href').trim(),
}
}).get()
困難的都研究完了,接下來就像組裝積木把它組起來爾已。
首先我們先來寫個抓取單一 url 的 function,因為這個 function 會用到 request,所以肯定會是一個非同步的 function,因此,我們除了要給 url 外,還要傳個 callback 進去。
const request = require('request')
function getInfo(url, callback){
request(url, function(err, res, body){
// 處理內容
})
}
接下來我們用 cheerio 來模擬 jQuery 的選擇器去選取我們要抓的資料,最後將拿到的資訊傳到 callback 裡面,那麼 getInfo function 就完成了。
const request = require('request')
const cheerio = require('cheerio')
function getInfo(url, callback){
request(url, function(err, res, body){
var $ = cheerio.load(body)
var link = url
var name = $('.profile-header__name').text().trim()
var title = $('.qa-list__title--ironman').text().trim().replace(' 系列', '')
var joinDays = $('.qa-list__info--ironman span').eq(0).text().replace(/[^0-9]/g,'')
var posts = $('.qa-list__info--ironman span').eq(1).text().replace(/[^0-9]/g,'')
var subscriber = $('.qa-list__info--ironman span').eq(2).text().replace(/[^0-9]/g,'')
var postList = $('.qa-list').map((index, obj) => {
return {
title: $(obj).find('.qa-list__title').text().trim(),
like: $(obj).find('.qa-condition__count').eq(0).text().trim(),
comment: $(obj).find('.qa-condition__count').eq(1).text().trim(),
view: $(obj).find('.qa-condition__count').eq(2).text().trim(),
date: $(obj).find('.qa-list__info-time').text().trim(),
url: $(obj).find('.qa-list__title a').attr('href').trim(),
}
}).get()
callback(null, {
name, title, link, joinDays, posts, subscriber, postList
});
})
}
接著我們會需要一個 array 來裝所有參賽者的主題網址。
const ironmans = [
'https://ithelp.ithome.com.tw/users/20107159/ironman/1325',
'https://ithelp.ithome.com.tw/users/20107356/ironman/1315',
'https://ithelp.ithome.com.tw/users/20107440/ironman/1355',
'https://ithelp.ithome.com.tw/users/20107334/ironman/1335',
'https://ithelp.ithome.com.tw/users/20107329/ironman/1286',
'https://ithelp.ithome.com.tw/users/20091297/ironman/1330',
'https://ithelp.ithome.com.tw/users/20075633/ironman/1375',
'https://ithelp.ithome.com.tw/users/20107247/ironman/1312',
'https://ithelp.ithome.com.tw/users/20107335/ironman/1337',
'https://ithelp.ithome.com.tw/users/20106699/ironman/1283',
'https://ithelp.ithome.com.tw/users/20107420/ironman/1381',
]
最後我們使用 async 的 map 來跑 ironmans,並讓 array 裡面的每個物件去呼叫剛剛寫的 getInfo function,這樣整體就完成了。
async.map( ironmans, getInfo, (err, results)=>{
console.log(results);
})
const request = require('request')
const async = require('async')
const cheerio = require('cheerio')
const ironmans = [
'https://ithelp.ithome.com.tw/users/20107159/ironman/1325',
'https://ithelp.ithome.com.tw/users/20107356/ironman/1315',
'https://ithelp.ithome.com.tw/users/20107440/ironman/1355',
'https://ithelp.ithome.com.tw/users/20107334/ironman/1335',
'https://ithelp.ithome.com.tw/users/20107329/ironman/1286',
'https://ithelp.ithome.com.tw/users/20091297/ironman/1330',
'https://ithelp.ithome.com.tw/users/20075633/ironman/1375',
'https://ithelp.ithome.com.tw/users/20107247/ironman/1312',
'https://ithelp.ithome.com.tw/users/20107335/ironman/1337',
'https://ithelp.ithome.com.tw/users/20106699/ironman/1283',
'https://ithelp.ithome.com.tw/users/20107420/ironman/1381',
]
async.map( ironmans, getInfo, (err, results)=>{
console.log(results);
})
function getInfo(url, callback){
request(url, function(err, res, body){
var $ = cheerio.load(body)
var link = url
var name = $('.profile-header__name').text().trim()
var title = $('.qa-list__title--ironman').text().trim().replace(' 系列', '')
var joinDays = $('.qa-list__info--ironman span').eq(0).text().replace(/[^0-9]/g,'')
var posts = $('.qa-list__info--ironman span').eq(1).text().replace(/[^0-9]/g,'')
var subscriber = $('.qa-list__info--ironman span').eq(2).text().replace(/[^0-9]/g,'')
var postList = $('.qa-list').map((index, obj) => {
return {
title: $(obj).find('.qa-list__title').text().trim(),
like: $(obj).find('.qa-condition__count').eq(0).text().trim(),
comment: $(obj).find('.qa-condition__count').eq(1).text().trim(),
view: $(obj).find('.qa-condition__count').eq(2).text().trim(),
date: $(obj).find('.qa-list__info-time').text().trim(),
url: $(obj).find('.qa-list__title a').attr('href').trim(),
}
}).get()
callback(null, {
name, title, link, joinDays, posts, subscriber, postList
});
})
}
既然已經可以抓取資料了,那麼我們可以找個地方放,讓他變成是一個 api service,可以選擇放在 aws lambda、google function、heroku,那麼就可以讓其他人直接取得資訊加以應用了。
http://ironmans.goodideas-studio.com/
這邊是好想工作室所有參賽者的主題資訊,也歡迎各位點閱追蹤。
你好,想知道你怎麼處理分頁裡的文章列表?
因為完整程式碼跟 http://ironmans.goodideas-studio.com/ 的結果不一樣。